package com.hdu.degrete.degrate;


import lombok.Data;
import org.aspectj.lang.ProceedingJoinPoint;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;


@Data
public class CircuitBreaker {


    public enum State {
        OPEN,
        HALF_OPEN,
        CLOSE
    }

    public enum DegrateType {
        /**
         * 自定义返回类型
         */
        CUSTOM_RESULT,
        /**
         * 抛异常
         */
        THROW_EXCEPTION
    }

    private final AtomicLong SUCCESS_COUNT = new AtomicLong(0);
    private final AtomicLong FAIL_COUNT = new AtomicLong(0);
    private final AtomicReference<State> STATE = new AtomicReference<>(State.CLOSE);
    private final long DEGRATE_TIME_INTERVAL;
    private long waitCloseTime;


    public CircuitBreaker(long degrateTimeInterval, TimeUnit degrateTimeIntervalUnit) {
        this.DEGRATE_TIME_INTERVAL = degrateTimeIntervalUnit.toMillis(degrateTimeInterval);
    }


    public boolean tryPass(ProceedingJoinPoint proceedingJoinPoint) {

        if (STATE.get() == State.CLOSE) {
            return true;
        } else if (STATE.get() == State.OPEN) {
            return retryTimeArrived() && tryOneTimeAndTryOpen2Close(proceedingJoinPoint);
        }

        return false;
    }


    public void tryFromCloseToOpen(double failRate) {
        if (getFailRate() >= failRate) {
            STATE.compareAndSet(State.CLOSE, State.OPEN);
            refreshNextRetryTime();
        }
    }

    private boolean tryOneTimeAndTryOpen2Close(ProceedingJoinPoint proceedingJoinPoint) {
        try {
            if (STATE.compareAndSet(State.OPEN, State.HALF_OPEN)) {
                proceedingJoinPoint.proceed();
                STATE.compareAndSet(State.HALF_OPEN, State.CLOSE);
                return true;
            } else {
                return false;
            }
        } catch (Throwable e) {
            STATE.set(State.OPEN);
            return false;
        }
    }


    public void refreshNextRetryTime() {
        waitCloseTime = System.currentTimeMillis() + DEGRATE_TIME_INTERVAL;
    }

    private boolean retryTimeArrived() {
        return System.currentTimeMillis() >= waitCloseTime;
    }

    public void success() {
        SUCCESS_COUNT.incrementAndGet();
    }

    public void fail() {
        FAIL_COUNT.incrementAndGet();
    }


    public double getFailRate() {
        return FAIL_COUNT.get() / (double) (FAIL_COUNT.get() + SUCCESS_COUNT.get());
    }
}
